﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Cfg.Attributes;
using Cfg.ConfigProviders;
using Cfg.ConfigValues;
using Cfg.Helpers;

namespace Cfg.Builders
{
    /// <summary>
    /// 
    /// </summary>
	public class ConfigBuilder
	{
		/// <summary>
		/// Validation context
		/// </summary>
		public readonly string Context;

        /// <summary>
        /// Validation errors
        /// </summary>
		private readonly List<ValidationError> _errors = new List<ValidationError>();

        /// <summary>
        /// All parsed config values
        /// </summary>
        private readonly Dictionary<string, object> _values = new Dictionary<string, object>(); 

        /// <summary>
        /// 
        /// </summary>
        private readonly List<ConfigPropInfo> _configPropInfos = new List<ConfigPropInfo>();

        /// <summary>
        /// Initial config key value pairs
        /// </summary>
        private readonly ConfigKeyValues _keyValuePairs; 

        /// <summary>
        /// 
        /// </summary>
	    private class ConfigPropInfo
	    {
	        public string Key { get; set; }
            public PropertyInfo PropertyInfo { get; set; }
            public object Instance { get; set; }
            public object[] CustomAttributes { get; set; }
	    }

        public ConfigBuilder(Type t, object instance, IConfigFileProvider provider, string context = null)
	    {
            Context = context;

            _keyValuePairs = provider.GetValues();
            LoadProperties(t, instance);
            Build();
	    }

	    /// <summary>
	    /// Load all public static properties with getter and setter method from given type.
	    /// </summary>
	    /// <param name="type"></param>
	    /// <param name="instance"></param>
	    /// <param name="prefix">Prefix that will be added to property key</param>
	    /// <returns></returns>
	    private void LoadProperties(Type type, object instance, string prefix = null)
		{
			// todo - avoid circular reference
            PropertyInfo[] properties = type.GetProperties();
            /*
		    if (false && type.IsInterface)
		    {
		        properties = type.GetProperties();
		    }
            else
            {
			    properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static | (BindingFlags.SetProperty & BindingFlags.GetProperty));
            }
            */

			foreach (var propertyInfo in properties)
			{
				if (!propertyInfo.CanWrite)
					continue;

                var customAttributes = instance == null
                        ? propertyInfo.GetCustomAttributes(false)
                        : propertyInfo.GetCustomAttributesFromInterfaces();

			    string originalKey = (prefix == null ? propertyInfo.Name : string.Format("{0}.{1}", prefix, propertyInfo.Name));
			    string key = GetMappedKey(customAttributes) ?? originalKey;

			    if (propertyInfo.PropertyType.IsInterface)
			    {
			        var proxy = propertyInfo.GetValue(instance, null);
			        LoadProperties(proxy.GetType(), proxy, key);
			    }
                /*
                else if (propertyInfo.PropertyType.IsArray)
                {
                    
                }*/
			    else
			    {
			        _configPropInfos.Add(new ConfigPropInfo
			        {
			            Key = key,
			            PropertyInfo = propertyInfo,
			            Instance = instance,
			            CustomAttributes = customAttributes
			        });
			    }
			}

			var subTypes = type.GetNestedTypes(BindingFlags.Instance | BindingFlags.Public);
			foreach (var subType in subTypes)
			{
				LoadProperties(subType, instance, prefix == null ? subType.Name : string.Format("{0}.{1}", prefix, subType.Name));
			}
		}

        private string GetMappedKey(object[] customAttributes)
        {
            var mapAttr = customAttributes.OfType<MapAttribute>().FirstOrDefault();
            if (mapAttr == null)
                return null;

            return mapAttr.Key;
        }

		/// <summary>
		/// Tries to set all configuration values and collects all errors.
		/// </summary>
		/// <returns></returns>
		private void Build()
		{
            foreach (var prop in _configPropInfos)
			{
				try
				{
                    var stringValue = _keyValuePairs[prop.Key];
                    /*
				    if (stringValue == null && prop.PropertyInfo.PropertyType.IsArray)
				    {

				    }
                    */
				    var finalizedStringValue = FinalizeStringValue(stringValue);

				    var valueBuilder = new ValueBuilder(
                        prop.PropertyInfo.PropertyType,
                        prop.CustomAttributes,
				        Context,
				        stringValue,
				        finalizedStringValue);

                    var value = valueBuilder.GetValue();
                    prop.PropertyInfo.SetValue(prop.Instance, value, null);
				    _values[prop.Key] = value;
				}
				catch (Exception ex)
				{
                    _errors.Add(new ValidationError(prop.Key, ex.Message));
				}
			}
		}

		/// <summary>
		/// Finalize string value. Replaces all placeholders with actual values.
		/// </summary>
		/// <param name="stringValue"></param>
		/// <returns></returns>
		private string FinalizeStringValue(string stringValue)
		{
			if (string.IsNullOrEmpty(stringValue))
				return stringValue;

			var regex = new Regex("(%[^%]*%)");
			var matches = regex.Matches(stringValue);
			foreach (Match match in matches)
			{
				var subkey = match.Value.Trim('%');
                var subValue = _keyValuePairs[subkey];
				stringValue = FinalizeStringValue(stringValue.Replace(match.Value, subValue));
			}

			return stringValue;
		}

        /// <summary>
        /// Errors occured during setting values
        /// </summary>
        /// <returns></returns>
	    public ValidationError[] GetErrors()
	    {
	        return _errors.ToArray();
	    }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
	    public object GetFieldValue(string key)
	    {
	        return _values[key];
	    }
	}
}
